﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace Raider.WinForms.Actions.Linkage
{
    /// <summary>
    /// Links actions with targets (client controls and components) and maintains a map of
    /// linked actions and targets. Target objects are automatically updated when action property changes,
    /// and objects are automatically unlinked and removed from a map when some of them are disposed.
    /// </summary>
    sealed class ActionLinker : IDisposable
    {
        private const BindingFlags MemberFilter =
            BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

        internal readonly Dictionary<IComponent, IAction> targetActionMap =
            new Dictionary<IComponent, IAction>();

        private bool enabled = true;

        /// <summary>
        /// Gets <see cref="IAction"/> linked with the specified target.
        /// </summary>
        public IAction this[IComponent client]
        {
            get
            {
                IAction action;
                return targetActionMap.TryGetValue(client, out action)
                    ? action : null;
            }
        }

        /// <summary>
        /// Enables or disables any updates between the actions and target objects,
        /// keeping initial links between the objects.
        /// </summary>
        public bool Enabled
        {
            get { return enabled; }
            set
            {
                if (enabled != value)
                {
                    foreach (var kv in targetActionMap)
                    {
                        if (value) LinkInternal(kv.Key, kv.Value);
                        else UnLinkInternal(kv.Key, kv.Value);
                    }

                    enabled = value;
                }
            }
        }

        public void Link(IComponent client, IAction action)
        {
            if (targetActionMap.ContainsKey(client))
            {
                UnLink(client);
            }

            if (enabled)
            {
                LinkInternal(client, action);
            }

            client.Disposed += ClientDisposed;

            if (!targetActionMap.ContainsValue(action))
            {
                action.Disposed += ActionDisposed;
                action.PropertyChanged += ActionPropertyChanged;
            }

            targetActionMap.Add(client, action);
        }

        private static void LinkInternal(IComponent target, IAction action)
        {
            Type actionType = action.GetType();

            var actionMembers = actionType.GetMembers(MemberFilter);
            foreach (MemberInfo m in actionMembers)
            {
                var links = m.GetCustomAttributes(typeof(LinkAttribute), true);
                foreach (LinkAttribute l in links)
                {
                    var linker = l.CreateLinker(m);
                    if (linker.LinkMember(target, action))
                    {
                        break;
                    }
                }
            }
        }

        public void UnLink(IComponent client)
        {
            IAction action;
            if (!targetActionMap.TryGetValue(client, out action))
            {
                return;
            }

            UnLinkInternal(client, action);

            client.Disposed -= ClientDisposed;
            
            targetActionMap.Remove(client);
        }

        private static void UnLinkInternal(IComponent target, IAction action)
        {
            Type actionType = action.GetType();

            var actionMembers = actionType.GetMembers(MemberFilter);
            foreach (MemberInfo m in actionMembers)
            {
                var links = m.GetCustomAttributes(typeof(LinkAttribute), true);
                foreach (LinkAttribute l in links)
                {
                    var linker = l.CreateLinker(m);
                    if (linker.UnLinkMember(target, action))
                    {
                        break;
                    }
                }
            }
        }

        private void ActionPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (!enabled)
                return;

            IAction action = (IAction)sender;
            PropertyInfo actionProperty = action.GetType().GetProperty(e.PropertyName);

            var links = LinkWithPropertyAttribute.GetAttributes(actionProperty);
            if (links.Length > 0)
            {
                var targetsMap = targetActionMap.Where(kv => kv.Value == action);
                foreach (KeyValuePair<IComponent, IAction> kv in targetsMap)
                {
                    foreach (LinkWithPropertyAttribute l in links)
                    {
                        var linker = l.CreateLinker(actionProperty);
                        if (linker.LinkMember(kv.Key, action))
                        {
                            break;
                        }
                    }
                }
            }
        }

        private void ActionDisposed(object sender, EventArgs e)
        {
            var action = (IAction)sender;
            var links = targetActionMap.Where(kv => kv.Value == action).ToArray();
            for (int i = links.Length - 1; i >= 0; i--)
            {
                UnLink(links[i].Key);
            }
            
            action.Disposed -= ActionDisposed;
            action.PropertyChanged -= ActionPropertyChanged;
        }

        private void ClientDisposed(object sender, EventArgs e)
        {
            UnLink((IComponent)sender);
        }

        public void Dispose()
        {
            var targets = targetActionMap.Keys.ToArray();
            for (int i = targets.Length - 1; i >= 0; i--)
            {
                UnLink(targets[i]);
            }
        }
    }
}
